//	--------------------------------------------------------------------
//	Copyright(C) 2006,2007 Zhao Yukun. All rights reserved.
//	This file is a part of the Xeres system.
//	Xeres 2007
//	Contact : xeres.engine@gmail.com
//	$(reservedInfo)
//	$(reservedDeclaration)
//	--------------------------------------------------------------------
///	@file	<XEditCtrl.cpp>
///	@path	~/src/gui/xgui/
///	@date	2008/06/08
///	@desc	.

#define _FULLAPI
#include "config/config.h"

#include "lib/utilities/observer.h"

#include "gui/IGuiForm.h"
#include "gui/xgui/XEditCtrl.h"
#include "gui/composite/composition.h"

namespace xeres {

	IMPL_IOBJECT_CLASS_DYNAMIC( XEditCtrl );

	// XEditCtrl
	XEditCtrl::XEditCtrl( const Rect<int>& placement , Ref<StyleFont> font )
		: IGuiControl( placement , true , true , true )
		, m_nBorder( 3 )
		, m_nSpacing( 0 )
		, m_nCaret( 0 )
		, m_nSelStart( 0 )
		, m_nFirstVisible( 0 )
		, m_bCaretOn( true )
		, m_bEntered( false )
		, m_bInsertMode( true )
		, m_bMenuLoop( false )
		, m_bActive( false )
		, m_bMouseDrag( false )
		, m_isPassword( false )
		, m_bHideCaret( false )
		, m_text( font->hfont() )
		, m_font( font )
		, m_rect( placement )
		, m_caretFlash( 0 , 512 )
	{
	}

	// XEditCtrl
	XEditCtrl::XEditCtrl( void )
		: IGuiControl( Rect<int>() , true , true , true )
		, m_nBorder( 3 )
		, m_nSpacing( 0 )
		, m_nCaret( 0 )
		, m_nSelStart( 0 )
		, m_nFirstVisible( 0 )
		, m_bCaretOn( true )
		, m_bEntered( false )
		, m_bInsertMode( true )
		, m_bMenuLoop( false )
		, m_bActive( false )
		, m_bMouseDrag( false )
		, m_isPassword( false )
		, m_bHideCaret( false )
		, m_text( NULL )
		, m_font( NULL )
		, m_caretFlash( 0 , 512 )
	{
	}
	// ~XEditCtrl
	XEditCtrl::~XEditCtrl( void )
	{
	}

	// TextLeft
	inline int XEditCtrl::TextLeft( void ) const
	{
		return 4;
	}

	// TextTop
	inline int XEditCtrl::TextTop( void ) const
	{
		return 3;
	}

	// TextRight
	inline int XEditCtrl::TextRight( void ) const
	{
		return m_rect.width() - 10;
	}

	// TextBottom
	inline int XEditCtrl::TextBottom( void ) const
	{
		return m_rect.height() - 3;
	}

	// SetSize
	bool XEditCtrl::SetSize( const Pair<int>& size )
	{
		if( __super::SetSize( size ) )
		{
			m_rect = GetPlacement();
			return true;
		}
		return false;
	}
	// SetPlacement
	bool XEditCtrl::SetPlacement( const Rect<int>& placement )
	{
		if( __super::SetPlacement( placement ) )
		{
			m_rect = GetPlacement();
			return true;
		}
		return false;
	}
	// OnKeyInput
	HandleInput XEditCtrl::OnKeyInput( Updater& updater )
	{
		const KeyInput& input = updater.GetKeyInput();

		if( !input.IsPressed() )
			return INPUT_UNHANDLED;

		// short cuts
// 		if( m_hotkey.OnKeyInput( input ) == INPUT_HANDLED )
// 			return INPUT_HANDLED;

		HandleInput result = INPUT_UNHANDLED;

		switch( input.GetKey() )
		{
		case KeyInput::KEY_HOME:
			PlaceCaret( 0 );
			if( !input.IsShiftDown() )
				m_nSelStart = m_nCaret;
			ResetCaretBlink();
			result = INPUT_HANDLED;
			break;

		case KeyInput::KEY_END:
			PlaceCaret( (int)m_text.GetTextSize() );
			if( !input.IsShiftDown() )
				m_nSelStart = m_nCaret;
			ResetCaretBlink();
			result = INPUT_HANDLED;
			break;

		case KeyInput::KEY_INSERT:
			if( input.IsCtrlDown() )
			{
				// Control + Insert. Copy to clipboard.
				CopyToClipboard();
			}
			else if( input.IsShiftDown() )
			{
				// Shift + Insert. Paste from clipboard.
				PasteFromClipboard();
			}
			else
			{
				// Toggle caret insert mode.
				m_bInsertMode = !m_bInsertMode;
			}
			result = INPUT_HANDLED;
			break;

		case KeyInput::KEY_DELETE:
			// Check if there is a text selection.
			if( m_nCaret != m_nSelStart )
			{
				DeleteSelectionText();
			}
			else
			{
				 m_text.RemoveChar( m_nCaret );
			}
			ResetCaretBlink();
			OnText();
			result = INPUT_HANDLED;
			break;

		case KeyInput::KEY_NUMPAD4:
			if( input.IsNumLocked() )
				result = ProcessInput( input );
			break;
			// as left arrow if num lock is off

		case KeyInput::KEY_LEFTARROW:
			if( input.IsCtrlDown() )
			{
				// Control is down. Move the caret to a new item
				// instead of a character.
				m_text.GetPriorItemPos( m_nCaret, &m_nCaret );
				PlaceCaret( m_nCaret );
			}
			else if( m_nCaret > 0 )
			{
				PlaceCaret( m_nCaret - 1 );
			}
			if( !input.IsShiftDown() )
			{
				// Shift is not down. Update selection
				// start along with the caret.
				m_nSelStart = m_nCaret;
			}
			ResetCaretBlink();
			result = INPUT_HANDLED;
			break;

		case KeyInput::KEY_NUMPAD6:
			if( input.IsNumLocked() )
			{
				result = ProcessInput( input );
				OnText();
			}
			break;
			// as left arrow if num lock is off
		case KeyInput::KEY_RIGHTARROW:
			if( input.IsCtrlDown() )
			{
				// Control is down. Move the caret to a new item
				// instead of a character.
				m_text.GetNextItemPos( m_nCaret, &m_nCaret );
				PlaceCaret( m_nCaret );
			}
			else if( m_nCaret < (int)m_text.GetTextSize() )
			{
				PlaceCaret( m_nCaret + 1 );
			}
			if( !input.IsShiftDown() )
			{
				m_nSelStart = m_nCaret;
			}
			ResetCaretBlink();
			result = INPUT_HANDLED;
			break;

		case KeyInput::KEY_BACKSPACE:
			// If there's a selection, treat this
			// like a delete key.
			if( m_nCaret != m_nSelStart )
			{
				DeleteSelectionText();
			}
			else if( m_nCaret > 0 )
			{
				// Move the caret, then delete the char.
				PlaceCaret( m_nCaret - 1 );
				m_nSelStart = m_nCaret;
				m_text.RemoveChar( m_nCaret );
			}
			ResetCaretBlink();
			OnText();
			result = INPUT_HANDLED;
			break;

		case KeyInput::KEY_RETURN:
			// Return call.
			if( m_onDefault )
			{
				if( m_onDefault( true ) )
					return INPUT_HANDLED;
			}
			return INPUT_UNHANDLED;

		case KeyInput::KEY_ESCAPE:
			// Return call.
			if( m_onDefault )
			{
				if( m_onDefault( false ) )
					return INPUT_HANDLED;
			}
			return INPUT_UNHANDLED;

		default:
			// try to translate characters & input
			{
				result = ProcessInput(input);
			}
		}
		if( result == INPUT_HANDLED )
		{
			UpdateControl();
		}
		return result;
	}
	// OnMouseInput
	HandleInput XEditCtrl::OnMouseInput( Updater& updater )
	{
		const MouseInput& input = updater.GetMouseInput();

		Pair<int> localPt = updater.GetPoint();
		POINT pt = { localPt.x , localPt.y };
		Rect<int> bound( TextLeft() , TextTop() , TextRight() , TextBottom() );
		bound.offset( GetPosition() );

		HandleInput result = INPUT_UNHANDLED;
		
		switch( input.GetAction() )
		{
		case Input::ACTION_MOUSE_LB_DBCLICK:

			if( !HitTest( localPt ) )
				return INPUT_UNHANDLED;

			if( !GetFocus() )
			{
				GetParent()->EnterInputMode( this , updater );
			}

			if( m_nSelStart == m_nCaret )
			{
				m_nSelStart = 0;
				PlaceCaret( (int)m_text.GetTextSize() );
			}
			result = INPUT_HANDLED;
			break;

		case Input::ACTION_MOUSE_LB_CLICK:

			if( !HitTest( localPt ) )
				return INPUT_UNHANDLED;

			if( !bound.is_inside( localPt ) )
				return INPUT_UNHANDLED;

			if( !GetFocus() )
			{
				GetParent()->EnterInputMode( this , updater );
			}

			m_bMouseDrag = true;

			// Select by click.
			{

				// Determine the character corresponding to the coordinates.
				int nCP, nTrail, nX1st;
				m_text.CPtoX( m_nFirstVisible, FALSE, &nX1st );  // X offset of the 1st visible char

				int left = bound.left();

				if( m_text.XtoCP( pt.x - left + nX1st, &nCP, &nTrail ) )
				{
					// Cap at the NULL character.
					if( nTrail && nCP < (int)m_text.GetTextSize() )
						PlaceCaret( nCP + 1 );
					else
						PlaceCaret( nCP );

					if( !( input.IsShiftDown() ) )
					{
						m_nSelStart = m_nCaret;
					}
					ResetCaretBlink();
				}
			}

			result = INPUT_HANDLED;
			break;

		case Input::ACTION_MOUSE_LB_UP:
			m_bMouseDrag = false;

			result = INPUT_HANDLED;
			break;

		case Input::ACTION_MOUSE_RB_CLICK:
			if( !GetFocus() )
			{
				GetParent()->EnterInputMode( this , updater );
			}
			result = INPUT_HANDLED;
			break;

		case Input::ACTION_MOUSE_MOVE:

			if( m_bMouseDrag )
			{
				// Determine the character corresponding to the coordinates.
				int nCP, nTrail, nX1st;
				m_text.CPtoX( m_nFirstVisible, FALSE, &nX1st );  // X offset of the 1st visible char

				int left = bound.left();

				if( m_text.XtoCP( pt.x - left + nX1st, &nCP, &nTrail ) )
				{
					// Cap at the NULL character.
					if( nTrail && nCP < (int)m_text.GetTextSize() )
						PlaceCaret( nCP + 1 );
					else
						PlaceCaret( nCP );
				}

				result = INPUT_HANDLED;
			}
		}
		if( result == INPUT_HANDLED )
		{
			UpdateControl();
		}
		return result;
	}
	// OnFocusIn
	void XEditCtrl::OnFocusIn( Updater& updater , RefWeak<IGuiComp> last )
	{
		__super::OnFocusIn( updater , last );
		if( m_focus )
		{
			m_bActive = true;
			ResetCaretBlink( true );
			// set flashing
			m_caretFlash.Set( 0 );
			m_caretFlash.SetDest( 255 );
			m_parent->AddTickable( this );
		}
	}
	// OnFocusOut
	void XEditCtrl::OnFocusOut( Updater& updater , RefWeak<IGuiComp> next )
	{
		__super::OnFocusOut( updater , next );
		if( !m_focus )
		{
			m_bActive = false;
			// clear caret flashing
			m_caretFlash.Set( 0 , false );
		}
	}
	// ProcessInput
	HandleInput XEditCtrl::ProcessInput( const KeyInput &input )
	{
		if( input.IsAltDown() || input.IsCtrlDown() )
			return INPUT_UNHANDLED;

		wchar_t ch = input.GetInputChar();
		if( ch == '\0' )
			return INPUT_UNHANDLED;

		// If there's a selection and the user
		// starts to type, the selection should
		// be deleted.
		if( m_nCaret != m_nSelStart )
		{// overwrite
			DeleteSelectionText();
			m_nSelStart = m_nCaret;
		}

		if( !m_bInsertMode && ( m_nCaret < (int)m_text.GetTextSize() ) )
		{
			m_text[m_nCaret] = ch;
			PlaceCaret( m_nCaret + 1 );
			m_nSelStart = m_nCaret;
		}
		else
		{
			// If we are in overwrite mode and there is already
			// a char at the caret's position, simply replace it.
			// Otherwise, we insert the char as normal.
			if( m_text.InsertChar( m_nCaret, ch ) )
			{
				PlaceCaret( m_nCaret + 1 );
				m_nSelStart = m_nCaret;
			}
		}
		ResetCaretBlink();
		OnText();
		return INPUT_HANDLED;
	}
	// SetFont
	void XEditCtrl::SetFont( RefWeak<StyleFont> font )
	{
		m_text.SetFont( font->hfont() );
	}
	// SetPassword
	void XEditCtrl::SetPassword( void )
	{
		m_isPassword = true;
		m_text.SetPassword();
	}
	// SelectAll
	void XEditCtrl::SelectAll( void )
	{
		m_nSelStart = 0;
		PlaceCaret( (int)m_text.GetTextSize() );
	}
	// SetCaretTimer
	void XEditCtrl::SetCaretTimer( void )
	{

	}
	// ClearText
	void XEditCtrl::ClearText()
	{
		m_text.Clear();
		m_nFirstVisible = 0;
		PlaceCaret( 0 );
		m_nSelStart = 0;
		OnText();
	}
	// SetText
	void XEditCtrl::SetText( const WString& text , bool bSelected )
	{
		m_text.SetText( text );
		m_nFirstVisible = 0;
		// Move the caret to the end of the text
		PlaceCaret( (int)m_text.GetTextSize() );
		m_nSelStart = bSelected ? 0 : m_nCaret;
		OnText();
	}
	// CopyToClipboard
	void XEditCtrl::CopyToClipboard( void )
	{
		// Copy the selection text to the clipboard
		if( m_nCaret != m_nSelStart && ::OpenClipboard( NULL ) )
		{
			::EmptyClipboard();

			HGLOBAL hBlock = ::GlobalAlloc( GMEM_MOVEABLE, sizeof(WCHAR) * ( m_text.GetTextSize() + 1 ) );
			if( hBlock )
			{
				WCHAR * pwszText = (WCHAR*)::GlobalLock( hBlock );
				if( pwszText )
				{
					int nFirst = __min( m_nCaret, m_nSelStart );
					int nLast = __max( m_nCaret, m_nSelStart );
					if( nLast - nFirst > 0 )
						::CopyMemory( pwszText, m_text.GetBuffer() + nFirst, (nLast - nFirst) * sizeof(WCHAR) );
					pwszText[nLast - nFirst] = 0;  // Terminate it
					::GlobalUnlock( hBlock );
				}
				::SetClipboardData( CF_UNICODETEXT, hBlock );
			}
			::CloseClipboard();
			// We must not free the object until CloseClipboard is called.
			if( hBlock )
				::GlobalFree( hBlock );
		}
	}
	// PasteFromClipboard
	void XEditCtrl::PasteFromClipboard( void )
	{
		if( ::OpenClipboard( NULL ) )
		{
			HANDLE handle = ::GetClipboardData( CF_UNICODETEXT );
			if( handle )
			{
				// Convert the ANSI string to Unicode, then
				// insert to our buffer.
				WCHAR *pwszText = (WCHAR*)::GlobalLock( handle );
				if( pwszText )
				{
					if( wcslen( pwszText ) > 0 )
					{
						int nFirst = __min( m_nCaret, m_nSelStart );
						int nLast = __max( m_nCaret, m_nSelStart );
						// Update caret and selection
						//				PlaceCaret( nFirst );
						//				m_nSelStart = m_nCaret;
						if( m_text.InsertString( m_nCaret, pwszText ) )
							PlaceCaret( m_nCaret + lstrlenW( pwszText ) );
						m_nSelStart = m_nCaret;
					}
					::GlobalUnlock( handle );
				}
			}
			::CloseClipboard();
		}
		else
		{
			DeleteSelectionText();
		}
	}
	// GetRectText
	void XEditCtrl::GetRectText( RECT& rc )
	{
		rc.left = TextLeft();
		rc.top = TextTop();
		rc.right = TextRight();
		rc.bottom = TextBottom();
	}
	// PlaceCaret
	void XEditCtrl::PlaceCaret( int nCP )
	{
		assert( nCP >= 0 && nCP <= (int)m_text.GetTextSize() );
		m_nCaret = nCP;

		RECT rcText;
		GetRectText( rcText );

		// Obtain the X offset of the character.
		int nX1st, nX, nX2;
		m_text.CPtoX( m_nFirstVisible, FALSE, &nX1st );  // 1st visible char
		m_text.CPtoX( nCP, FALSE, &nX );  // LEAD
		// If nCP is the NULL terminator, get the leading edge instead of trailing.
		if( nCP == m_text.GetTextSize() )
			nX2 = nX;
		else
			m_text.CPtoX( nCP, TRUE, &nX2 );  // TRAIL

		// If the left edge of the char is smaller than the left edge of the 1st visible char,
		// we need to scroll left until this char is visible.
		if( nX <= nX1st )
		{
			// Simply make the first visible character the char at the new caret position.
			if( nCP > 0 )
				m_nFirstVisible = nCP - 1;
			else
				m_nFirstVisible = nCP;
		}
		// If the right of the character is bigger than the offset of the control's
		// right edge, we need to scroll right to this character.
		else if( nX2 > nX1st + RectWidth( rcText ) )
		{
			// Compute the X of the new left-most pixel
			int nXNewLeft = nX2 - RectWidth( rcText );

			// Compute the char position of this character
			int nCPNew1st, nNewTrail;
			m_text.XtoCP( nXNewLeft, &nCPNew1st, &nNewTrail );

			// If this coordinate is not on a character border,
			// start from the next character so that the caret
			// position does not fall outside the text rectangle.
			int nXNew1st;
			m_text.CPtoX( nCPNew1st, FALSE, &nXNew1st );
			if( nXNew1st < nXNewLeft )
				++nCPNew1st;

			m_nFirstVisible = nCPNew1st;
		}
	}
	// DeleteSelectionText
	void XEditCtrl::DeleteSelectionText( void )
	{
		int nFirst = min<int>( m_nCaret, m_nSelStart );
		int nLast = max<int>( m_nCaret, m_nSelStart );
		// Update caret and selection
		PlaceCaret( nFirst );
		m_nSelStart = m_nCaret;
		// Remove the characters
		for( int i = nFirst; i < nLast; ++i )
			m_text.RemoveChar( nFirst );
	}
	// ResetCaretBlink
	void XEditCtrl::ResetCaretBlink( bool ResetTimer /*= false */ )
	{
		m_bCaretOn = true;
	}
	// OnText
	void XEditCtrl::OnText( void )
	{
		if( m_onChange )
			m_onChange();
	}
	// Tick
	void XEditCtrl::Tick( Updater& updater , float dTime )
	{
		if( !GetFocus() )
		{
			m_parent->DelTickable( this );
		}
		else
		{
			// periodic
			if( m_caretFlash.IsEnded() )
			{
				if( m_caretFlash.Get() == 0 )
					m_caretFlash.Set( 255 );
				else
					m_caretFlash.Set( 0 );
			}
			m_caretFlash.Tick( dTime );
		}
		UpdateControl();
	}
	// Draw
	void XEditCtrl::Draw( Composition& composition )
	{
		//COUNTER_GUARD( _S("edit-draw") );

		using namespace Gdiplus;
		composition.graphics().DrawRectangle( &Pen( &SolidBrush( Color(255,255,255) ) ) , toRect( GetRect() ) );

		//
		// Compute the X coordinates of the first visible character.
		//
		int nXFirst = 0;
		m_text.CPtoX( m_nFirstVisible, FALSE, &nXFirst );

		int nSelStartX = 0, nCaretX = 0;  // Left and right X coordinates of the selection region

		//
		// Compute the X coordinates of the selection rectangle
		//
		m_text.CPtoX( m_nCaret, FALSE, &nCaretX );
		if( m_nCaret != m_nSelStart )
			m_text.CPtoX( m_nSelStart, FALSE, &nSelStartX );
		else
			nSelStartX = nCaretX;

		//
		// compute the selection rectangle
		//
		Rect<int> rcSelection;  // Make this available for rendering selected text
		if( m_nCaret != m_nSelStart )
		{
			int nSelLeftX = nCaretX, nSelRightX = nSelStartX;
			// Swap if left is bigger than right
			if( nSelLeftX > nSelRightX )
			{
				int nTemp = nSelLeftX;
				nSelLeftX = nSelRightX;
				nSelRightX = nTemp;
			}
			rcSelection.set( nSelLeftX, TextTop(), nSelRightX , TextBottom() );
			rcSelection.offset( Pair<int>( TextLeft() - nXFirst, 0 ) );
			rcSelection.set( rcSelection.left() , rcSelection.top() + 2 , rcSelection.right() , rcSelection.bottom() - 2 );
		}

		// Start the rectangle with insert mode caret
		Rect<int> rcCaret( TextLeft() - nXFirst + nCaretX , TextTop() + 1,
			TextLeft() - nXFirst + nCaretX + 2, TextBottom() - 1 );

		if( !m_bInsertMode )
		{
			// Obtain the right edge X coord of the current character
			int nRightEdgeX;
			m_text.CPtoX( m_nCaret, TRUE, &nRightEdgeX );
			rcCaret.set_right( TextLeft() - nXFirst + nRightEdgeX );
		}

		// If we are in overwrite mode, adjust the caret rectangle
		// to fill the entire character.
		if( !m_bInsertMode )
		{
			// Obtain the right edge X coord of the current character
			int nRightEdgeX;
			m_text.CPtoX( m_nCaret, TRUE, &nRightEdgeX );
			rcCaret.set_right( TextLeft() - nXFirst + nRightEdgeX );
		}

		// save current clip
		Rect<int> bound( TextLeft() , TextTop() , TextRight() + 6 , TextBottom() + 1 );

		ClippingGuard __clipguard( composition );
		composition.graphics().SetClip( toRect(bound) , Gdiplus::CombineModeIntersect );

		int alpha = (int)m_caretFlash.Get();
		if( GetFocus() && !m_bInsertMode && m_nCaret != m_nSelStart )
		{
			composition.graphics().FillRectangle( &Gdiplus::SolidBrush( Gdiplus::Color( alpha , 200 , 200 , 200 ) ) ,
				rcSelection.left() , rcSelection.top() , rcSelection.width() , rcSelection.height() );
		}
		else
		{
			composition.graphics().FillRectangle( &Gdiplus::SolidBrush( Gdiplus::Color( 166 , 80 , 100 , 111 ) ) ,
				rcSelection.left() , rcSelection.top() , rcSelection.width() , rcSelection.height() );

			if( GetFocus() )
			{
				composition.graphics().FillRectangle( &Gdiplus::SolidBrush( Gdiplus::Color( alpha , 200 , 200 , 200 ) ) ,
					rcCaret.left() , rcCaret.top() , rcCaret.width() , rcCaret.height() );
			}
		}

		__clipguard.RestoreClip();

		ARGB col;
		col.r = col.g = col.b = col.a = 255;

		Rect<int> rt( 0 , 0 , m_rect.width() , m_rect.height() );

		composition.painter().DrawShadowText( col , ARGB(133,0,0,0) , m_font , GetText().c_str() + m_nFirstVisible , -1 ,
 			bound , rt , DT_LEFT| DT_VCENTER | DT_SINGLELINE | DT_EXPANDTABS , 1 , 1 );

//  		composition.painter().DrawText( col , m_font , GetText().c_str() + m_nFirstVisible , -1 ,
// 	  			bound , rt , DT_LEFT| DT_VCENTER | DT_SINGLELINE | DT_EXPANDTABS );
	}


} // namespace xeres
